class XRegExp {
  private regExp: RegExp;
  private table = new Map();

  constructor(source: Record<string, RegExp | string>, root: string) {
    this.regExp = this.compileRegExp(source, root, 0).regExp;
  }

  exec(source: string) {
    let result = this.regExp.exec(source);

    if (result === null) {
      throw new Error(`Invalid input ${source}`);
    }

    for (let i = 1; i < result.length; i++) {
      if (typeof result[i] !== 'undefined') {
        result[this.table.get(i - 1)] = result[i];
      }
    }

    return result;
  }

  compileRegExp<
    U extends Record<string, RegExp | string>,
    T extends keyof U,
    >(
      regExp: U, name: T, start: number,
  ): {
    regExp: RegExp;
    length: number;
  } {
    const value = regExp[name];

    if (value instanceof RegExp) {
      return {
        regExp: value,
        length: 0,
      };
    }

    let length = 0;

    const source = new RegExp(
      value.replace(/<([^>]+)>/g, (substring: string, $1: string) => {
        this.table.set(start + length, $1);
        this.table.set($1, start + length);

        ++length;

        const r = this.compileRegExp(regExp, $1, start + length);

        length += r.length;
        return `(${r.regExp.source})`;
      }),
      'g',
    );

    return {
      regExp: source,
      length,
    }
  }

  get lastIndex(): number {
    return this.regExp.lastIndex;
  }

  set lastIndex(value: number) {
    this.regExp.lastIndex = value;
  }
}

const xRegExp = {
  InputElement: "<WhiteSpace>|<LineTerminator>|<Comment>|<CommonToken>",

  WhiteSpace: / /,
  LineTerminator: /\r|\n/,

  Comment: "<SingleLineComment>|<MultiLineComment>",
  SingleLineComment: /\/\/[^\r\n]*/,
  MultiLineComment: /\/\*(?:[^*]|\*[^\/])*\*\//,

  CommonToken: "<Literal>|<ReservedWord>|<IdentifierName>|<Punctuator>",

  Literal: "<NumericLiteral>|<StringLiteral>|<BooleanLiteral>|<NullLiteral>",

  NumericLiteral: /(?:[1-9][0-9]*|0)(?:\.[0-9]*)?|\.[0-9]+/,
  BooleanLiteral: /true|false/,
  StringLiteral: /\"(?:[^"\r\n]|\\[\s\S])*\"|\'(?:[^'\r\n]|\\[\s\S])*\'/,
  NullLiteral: /null/,

  IdentifierName: /[a-zA-Z_$][a-zA-Z0-9_$]*/,

  ReservedWord: /if|else|for|while|function|this|var/,

  Punctuator: /{|}|\(|\)|\[|\]|\.|\.\.\.|;|,|<|>|<=|>=|===|!==|==|!=|\+|-|\*|\/|%|\?|:|=/,
}


export function* scan(source: string) {
  const regExp = new XRegExp(xRegExp, 'InputElement');

  while (regExp.lastIndex < source.length) {
    const result: any = regExp.exec(source);

    if (result.WhiteSpace || result.LineTerminator || result.Comment) {

    } else if (result.NumericLiteral) {
      yield {
        type: 'NumericLiteral',
        value: result[0],
      };
    } else if (result.BooleanLiteral) {
      yield {
        type: 'BooleanLiteral',
        value: result[0],
      };
    } else if (result.StringLiteral) {
      yield {
        type: 'StringLiteral',
        value: result[0],
      };
    } else if (result.NullLiteral) {
      yield {
        type: 'NullLiteral',
        value: null,
      };
    } else if (result.IdentifierName) {
      yield {
        type: 'IdentifierName',
        value: result[0],
      };
    } else if (result.ReservedWord) {
      yield {
        type: result[0],
      };
    } else if (result.Punctuator) {
      yield {
        type: result[0],
      };
    } else {
      throw new Error('Unexpected token ' + result[0]);
    }

    if (result.length === 0) {
      break;
    }
  }

  yield {
    type: 'EOF',
  };
}